<?php

// Namespace

namespace Loop;


// Dependencies

require_once(PATH_LOOP . 'Model/CUserGroupClassPermission.php');
require_once(PATH_LOOP . 'Model/CUserGroupObjectPermission.php');
require_once(PATH_LOOP . 'Model/CUserGroupViewPermission.php');


/**
 * CSecurityManager is a class that handles everything related to user rights
 * It is used by classes, objects and views everytime certain operations need to be performed
 * For instance, before rendering a view, CView itself checks whether it can be authorized
 * The same applies to CObject, in which calls to load(), save() and delete() are all subject to similar verifications
 */

class CSecurityManager
{
	// Properties

	private $permissionsOnViewsByUserGroup = null;
	private $defaultPermissionsByIpAndKey = null;


	/**
	 * Constructs a security manager
	 */

	public function __construct()
	{
		//

		$this->permissionsOnViewsByUserGroup = array();

		
		// Set the default permissions

		$this->defaultPermissionsByIpAndKey = array
		(
			'/[' . EXTENSION_APPLICATION . ']/Api/User.completeResetPassword' => array
			(
				'*/*' => true
			),
			'/[' . EXTENSION_APPLICATION . ']/Api/User.signIn' => array
			(
				'*/*' => true
			),
			'/[' . EXTENSION_APPLICATION . ']/Api/User.signOut' => array
			(
				'*/*' => true
			),
			'/[' . EXTENSION_APPLICATION . ']/Api/User.signUp' => array
			(
				'*/*' => true
			),
			'/[' . EXTENSION_APPLICATION . ']/Api/User.startResetPassword' => array
			(
				'*/*' => true
			),
			'/[' . EXTENSION_APPLICATION . ']/Api/User.validate' => array
			(
				'*/*' => true
			),
			'Javascript' => array
			(
				'*/*' => true
			),
			'Stylesheet' => array
			(
				'*/*' => true
			)
		);


		// If in local mode, always authorize access to admin

		if (env() === 'Local')
		{
			$this->defaultPermissionsByIpAndKey = array_merge
			(
				$this->defaultPermissionsByIpAndKey,
				array
				(
					'/[Loop]/Html/Page.Admin.html' => array
					(
						'*/*' => true
					),
					'/[Loop]/Html/Page.Admin.Documentation.html' => array
					(
						'*/*' => true
					),
					'/[Loop]/Html/Page.Admin.Profiling.html' => array
					(
						'*/*' => true
					),
					'/[Loop]/Html/Page.Admin.Sandbox.html' => array
					(
						'*/*' => true
					),
					'/[Loop]/Html/Page.Admin.Test.html' => array
					(
						'*/*' => true
					),
					'/[Loop]/Html/Page.Admin.Tickets.html' => array
					(
						'*/*' => true
					)
				)
			);
		}
	}


	/**
	 * Retrieves all permissions on classes for a given user group
	 *
	 * @param	int 	$userGroupId 	The user group ID
	 *
	 * @return	array 	An array with consolidated permissions
	 */
	
	public function consolidatePermissionsOnClassesByUserGroup($userGroupId)
	{
	}
	
	
	/**
	 * Retrieves all permissions on objects for a given user group
	 *
	 * @param	int		$userGroupId	The user group ID
	 *
	 * @return	array 	An array with consolidated permissions
	 */
	
	public function consolidatePermissionsOnObjectsByUserGroup($userGroupId)
	{
	}
	
	
	/**
	 * Retrieves all permissions on views for a given IP+key
	 *
	 * @param	string	$ip		The IP
	 * @param	string	$key	The key
	 *
	 * @return	array			An array with consolidated permissions
	 */
	
	public function consolidatePermissionsOnViewsByIpAndKey($ip, $key)
	{
	}
	
	
	/**
	 * Retrieves all permissions on views for a given user group
	 *
	 * @param	int		$userGroupId	The user group ID
	 *
	 * @return	array 	An array with consolidated permissions
	 */
	
	public function consolidatePermissionsOnViewsByUserGroup($userGroupId)
	{
		// Dependencies
		
		require_once(PATH_LOOP . 'Model/CUserGroupViewPermission.php');
		
		
		// Get permissions for this user group

		$userGroupViewPermission = new \Loop\CUserGroupViewPermission();

		$rawUserGroupViewPermissions = $userGroupViewPermission->find
		(
			array
			(
				'conditions' => array
				(
					'userGroupId' => array(EQUAL, $userGroupId),
				),
				'properties' => array('action', 'isEnabled', 'viewPath')
			)
		);


		// Store each in a more elegant manner

		foreach ($rawUserGroupViewPermissions as $rawUserGroupViewPermission)
		{
			// Load the raw object

			$userGroupViewPermission->loadFromRawObject($rawUserGroupViewPermission);


			// Store the permission by view / action

			if (isset($this->permissionsOnViewsByUserGroup[$userGroupViewPermission->getViewPath()]) == false)
			{
				$this->permissionsOnViewsByUserGroup[$userGroupViewPermission->getViewPath()] = array();
			}

			$this->permissionsOnViewsByUserGroup[$userGroupViewPermission->getViewPath()][$userGroupViewPermission->getAction()] = $userGroupViewPermission->getIsEnabled();
		}
	}

	
	/**
	 * Prevents user without permission to perform an action on a class
	 *
	 * @param	string 	$className 	The name of the class
	 * @param	string	$action		The action to be performed
	 * @param	object 	$user		The user (optional)
	 */
	
	public function grantPermissionOnClass($className, $action, &$user = null)
	{
		// If in CLI mode, grant permission

		if (app()->isRunningCli() === true)
		{
			return;
		}

		
		// Is security feature enabled?
		
		if (pref('[Loop]/features/permissions') == false)
		{
			return;
		}
		
		
		// If no user provided, use the current one
		
		if (is_null($user) == true)
		{
			$user = user();
		}
		
		
		// Do we have permission to perform this action on the class?
		
		$isPermissionGranted = $this->isPermissionOnClassByUserGroupGranted($className, $action, $user->getUserGroupId());
		
		
		// Last chance to get out of here with honors
		
		if ($isPermissionGranted == true)
		{
			return;
		}
		
		
		// No permission = kicked out!
		
		e
		(
			EXCEPTION_PERMISSION_ON_CLASS_NOT_GRANTED,
			array
			(
				'action' => $action,
				'className' => $className,
				'userGroupId' => $user->getUserGroupId(),
				'userId' => $user->getId()
			)
		);
	}
	
	
	/**
	 * Prevents user without permission to perform an action on an object
	 *
	 * @param	string	$className	The name of the class
	 * @param	int		$objectId	The ID of the object
	 * @param	string	$action		The action to be performed
	 * @param	object	$user		The user (optional)
	 */
	
	public function grantPermissionOnObject($className, $objectId, $action, &$user = null)
	{
		// If in CLI mode, grant permission

		if (app()->isRunningCli() === true)
		{
			return;
		}

		
		// Is security feature enabled?
		
		if (pref('[Loop]/features/permissions') == false)
		{
			return;
		}
		
		
		// If no user provided, use the current one
		
		if (is_null($user) == true)
		{
			$user = user();
		}
		
		
		// Do we have the right to perform this action on the class?
		
		app()->getSecurityManager()->grantPermissionOnClass($className, $action, $user);
		
		
		// Do we have permission to perform this action on the object?
		
		$isPermissionGranted = $this->isPermissionOnObjectByUserGroupGranted($className, $objectId, $action, $user->getUserGroupId());
		
		
		// Last chance to get out of here with honors
		
		if ($isPermissionGranted == true)
		{
			return;
		}
		
		
		// No permission = kicked out!
		
		e
		(
			EXCEPTION_PERMISSION_ON_OBJECT_NOT_GRANTED,
			array
			(
				'action' => $action,
				'className' => $className,
				'objectId' => $objectId,
				'userGroupId' => $user->getUserGroupId(),
				'userId' => $user->getId()
			)
		);
	}
	
	
	/**
	 * Prevents user without permission to perform an action on a view
	 *
	 * @param	CView 	$view		 	The view
	 * @param	string 	$action		 	The action to be performed
	 * @param	CUser 	$user		 	The user (optional)
	 * @param	bool 	$forceCheck 	True to ignore the permission flag, false otherwise
	 */
	
	public function grantPermissionOnView(&$view, $action, &$user = null, $forceCheck = false)
	{
		// If in CLI mode, grant permission

		if (app()->isRunningCli() === true)
		{
			return;
		}

		
		// Is security feature enabled?
		
		if
		(
			($forceCheck == false) &&
			(pref('[Loop]/features/permissions') == false)
		)
		{
			return;
		}
		
		
		// If it's a child view, perform no check
		
		if
		(
			(isset($view) === false) ||
			($view->isRoot === false)
		)
		{
			return;
		}
		
		
		// If no user provided, use the current one
		
		if (is_null($user) == true)
		{
			$user = user();
		}
		
		
		// At first we have nothing
		
		$isPermissionGranted = null;
		$isPermissionByIpAndKeyGranted = null;
		$isPermissionByUserGroupGranted = null;
		
		
		// Do we have permission to render with IP+key?
		
		$isPermissionByIpAndKeyGranted = $this->isPermissionOnViewByIpAndKeyGranted($view, $action);
			
		if ($isPermissionByIpAndKeyGranted == false)
		{
			// Do we have permission by user group?
			
			$isPermissionByUserGroupGranted = $this->isPermissionOnViewByUserGroupGranted($view, $action, $user->getUserGroupId());
		}
		
		
		// Last chance to get out of here with honors
		
		if
		(
			($isPermissionByIpAndKeyGranted == false) &&
			($isPermissionByUserGroupGranted == false)
		)
		{
			// No permission = kicked out!
			
			e
			(
				EXCEPTION_PERMISSION_ON_VIEW_NOT_GRANTED,
				array
				(
					'action' => $action,
					'isPermissionByIpAndKeyGranted' => intval($isPermissionByIpAndKeyGranted),
					'isPermissionByUserGroupGranted' => intval($isPermissionByUserGroupGranted),
					'userGroupId' => $user->getUserGroupId(),
					'userId' => $user->getId(),
					'viewPath' => $view->getPath()
				)
			);
		}
	}
	
	
	/**
	 * Initializes the security manager
	 */
	
	public function initialize()
	{
		try
		{
			// If using an API key, define the corresponding universe

			$key = fv('key', false);
			$apiKeys = pref('[Loop]/permissions/apiKeys');
			
			if (isset($apiKeys[$key]) === true)
			{
				$universe = $apiKeys[$key];
				
				if (empty($universe) === false)
				{
					pref('[Loop]/application/universe', $universe, true);
				}
			}


			// Store the permissions preference and disable it for now (we need to a few things on the database)
			
			$preferenceFeaturePermissions = pref('[Loop]/features/permissions');
			pref('[Loop]/features/permissions', false);
			
			
			// Sign user in from session
			
			$user = user();
			
			$user->signInFromSource();
			
			
			// Now let's grab all permissions related to the user
			
			$userGroupId = $user->getUserGroupId();
			
			$this->consolidatePermissionsOnClassesByUserGroup($userGroupId);
			$this->consolidatePermissionsOnObjectsByUserGroup($userGroupId);
			$this->consolidatePermissionsOnViewsByIpAndKey(v('SERVER::REMOTE_ADDR'), fv('key', false));
			$this->consolidatePermissionsOnViewsByUserGroup($userGroupId);
			
			
			// Reset preference to initial value
			
			pref('[Loop]/features/permissions', $preferenceFeaturePermissions);
		}
		catch (\Exception $e)
		{
			// It failed, probably due to the sign-in
			// However maybe we can still render the view
			
			try
			{
				$view = view();

				$this->grantPermissionOnView
				(
					$view,
					ACTION_RENDER,
					$user,
					$preferenceFeaturePermissions
				);
			}
			catch (\Exception $e2)
			{
				throw $e;
			}
		}
	}
	
	
	/**
	 * Checks whether permission by user group is granted on a class
	 *
	 * @param	string 	$class		 	The view
	 * @param	string 	$action		 	The action to be granted
	 * @param	int	 	$userGroupId 	The ID of the user group
	 *
	 * @return	bool 	True if permission granted, false if not granted, null if nothing had been defined
	 */
	
	public function isPermissionOnClassByUserGroupGranted($className, $action, $userGroupId)
	{
		// @todo Not finished yet, always grant permission
		
		return true;
		
		
		// Dependencies
		
		require_once(PATH_LOOP . 'Model/CUserGroupClassPermission.php');
		
		
		// We need to find all user group permissions on this class
		
		$userGroupClassPermission = new \Loop\CUserGroupClassPermission();
		
		$rawUserGroupClassPermissions = $userGroupClassPermission->find
		(
			array
			(
				'conditions' => array
				(
					'className' => array(LIKE, $className),
					'action' => array(EQUAL, $action),
					'userGroupId' => array(EQUAL, $userGroupId)
				),
				'properties' => array('isEnabled')
			)
		);
		
		
		// There should be only one result
		
		$nbPermissions = count($rawUserGroupClassPermissions);
		
		if ($nbPermissions > 1)
		{
			e
			(
				EXCEPTION_OBJECT_TOO_MANY_FOUND,
				array
				(
					'className' => $className,
					'nbPermissions' => $nbPermissions,
					'action' => $action,
					'userGroupId' => $userGroupId
				)
			);
		}
		
		
		// Parse rights returned and see whether we have a match
		
		$isPermissionGranted = null;
		
		foreach ($rawUserGroupClassPermissions as $rawUserGroupClassPermission)
		{
			// Load the right
			
			$userGroupClassPermission->loadFromRawObject($rawUserGroupClassPermission);
			
			
			// Do we have permission?
			
			$isPermissionGranted = ($userGroupClassPermission->getIsEnabled() == true);
		}
		
		
		return ($isPermissionGranted === true);
	}
	
	
	/**
	 * Checks whether permission by user group is granted on a class
	 *
	 * @param	string 	$class		 	The view
	 * @param	int	 	$objectId	 	The ID of the object
	 * @param	string 	$action		 	The action to be granted
	 * @param	int	 	$userGroupId 	The ID of the user group
	 *
	 * @return	bool 	True if permission granted, false if not granted, null if nothing had been defined
	 */
	
	public function isPermissionOnObjectByUserGroupGranted($className, $objectId, $action, $userGroupId)
	{
		// @todo Not finished yet, always grant permission
		
		return true;
	}
	
	
	/**
	 * Checks whether permission by IP+key is granted on a view
	 *
	 * @param	string 	$view		The view
	 * @param	string 	$action 	The action to be granted
	 *
	 * @return	bool 	True if permission granted, null if nothing had been defined
	 */
	
	public function isPermissionOnViewByIpAndKeyGranted($view, $action)
	{
		// Get the view type and path
		
		$viewPath = $view->getPath(true);
		$viewType = $view->type;


		// Hack to ensure APIs and actions are treated the same way

		if ($viewType === VIEW_TYPE_ACTION)
		{
			$viewPath = str_replace('/' . VIEW_TYPE_ACTION . '/', '/' . VIEW_TYPE_API . '/', $viewPath);
			$viewType = VIEW_TYPE_API;
		}


		//

		$ipAndKeyRights = array_merge
		(
			pref('[Loop]/permissions/ipAndKey/rights'),
			$this->defaultPermissionsByIpAndKey
		);


		//

		if (isset($ipAndKeyRights[$viewType]) == true)
		{
			$ipAndKeyRights = $ipAndKeyRights[$viewType];
		}
		else if (isset($ipAndKeyRights[$viewPath]) == true)
		{
			$ipAndKeyRights = $ipAndKeyRights[$viewPath];
		}
		else
		{
			$ipAndKeyRights = array();
		}


		// We have 4 different accepted pairs
		
		$pairs = array
		(
			v('SERVER::REMOTE_ADDR') . '/' . fv('key'),
			v('SERVER::REMOTE_ADDR') . '/*',
			'*/' . fv('key', false),
			'*/*'
		);


		// For each pair
		
		foreach ($pairs as $pair)
		{
			// Do we have a permission enabled? We're good to go!
			
			if
			(
				(isset($ipAndKeyRights[$pair]) === true) &&
				($ipAndKeyRights[$pair] == true)
			)
			{
				return true;
				break;
			}
		}

		
		return false;
	}
	
	
	/**
	 * Checks whether permission by user group is granted on a view
	 *
	 * @param	string	$view		 	The view
	 * @param	string	$action		 	The action to be granted
	 * @param	int		$userGroupId 	The ID of the user group
	 *
	 * @return	bool 	True if permission granted, false otherwise
	 */
	
	public function isPermissionOnViewByUserGroupGranted($view, $action, $userGroupId)
	{
		// Get the view type and path
		
		$viewPath = $view->getPath(true);
		$viewType = $view->type;


		// Hack to ensure APIs and actions are treated the same way

		if ($viewType === VIEW_TYPE_ACTION)
		{
			$viewPath = str_replace('/' . VIEW_TYPE_ACTION . '/', '/' . VIEW_TYPE_API . '/', $viewPath);
			$viewType = VIEW_TYPE_API;
		}


		// Try to get permissions by view type and path

		$isPermissionGrantedByViewType = null;
		$isPermissionGrantedByViewPath = null;

		if
		(
			(isset($this->permissionsOnViewsByUserGroup[$viewType]) == true) &&
			(isset($this->permissionsOnViewsByUserGroup[$viewType][$action]) == true)
		)
		{
			$isPermissionGrantedByViewType = ($this->permissionsOnViewsByUserGroup[$viewType][$action] == true);
		}
		
		if
		(
			(isset($this->permissionsOnViewsByUserGroup[$viewPath]) == true) &&
			(isset($this->permissionsOnViewsByUserGroup[$viewPath][$action]) == true)
		)
		{
			$isPermissionGrantedByViewPath = ($this->permissionsOnViewsByUserGroup[$viewPath][$action] == true);
		}


		// Do we have the permission? (path > type)

		if ($isPermissionGrantedByViewPath == true)
		{
			return true;
		}
		else if ($isPermissionGrantedByViewType == true)
		{
			return true;
		}
		else
		{
			return false;
		}
	}
}

?>
